﻿using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;

namespace NDF.XnaLibrary
{
	public class Camera
	{
		#region Fields

		//View matrix
		private Vector3 mPosition;
		private Vector3 mTarget;
		private Vector3 mUp;
		private Vector3 mForward;

		private float mYaw;
		private float mPitch;
		private float mRoll;
		
		//Projection matrix
		private float mFieldOfView;
		private float mAspectRatio;
		private float mNearPlane;
		private float mFarPlane;

		//Constants
		private readonly float MIN_FOV = MathHelper.ToRadians(5f);
		private readonly float MAX_FOV = MathHelper.Pi - 0.01f;
		private const float DefaultFoV = MathHelper.PiOver4;
		private const float DefaultAspectRatio = 4/3f;
		private const float DefaultNearPlane = 1f;
		private const float DefaultFarPlane = 100f;

		private Matrix mViewMatrix;
		private Matrix mProjectionMatrix;
		
		//The currently active camera
		private static Camera mActive;

		#endregion

		#region Constructors

		/// <summary>
		/// Initializes a new instance of the camera class with default settings.
		/// </summary>
		public Camera() :
			this(new Vector3(0, 0, 30), Vector3.Up, Vector3.Forward, DefaultFoV, DefaultAspectRatio, DefaultNearPlane, DefaultFarPlane)
		{ }

		/// <summary>
		/// Initializes a new instance of the camera class with the given position.
		/// </summary>
		/// <param name="position">The position of the camera.</param>
		public Camera(Vector3 position) :
			this(position, Vector3.Up, Vector3.Forward, DefaultFoV, DefaultAspectRatio, DefaultNearPlane, DefaultFarPlane)
		{ }

		/// <summary>
		/// Initializes a new instance of the camera class with the given settings.
		/// </summary>
		/// <param name="position">The position of the camera.</param>
		/// <param name="up">The vector the camera considers to be up.</param>
		/// <param name="forward">The vector the camera considers to be forward.</param>
		public Camera(Vector3 position, Vector3 up, Vector3 forward) :
			this(position, up, forward, DefaultFoV, DefaultAspectRatio, DefaultNearPlane, DefaultFarPlane)
		{ }

		/// <summary>
		/// Initializes a new instance of the camera class with the given settings.
		/// </summary>
		/// <param name="position">The position of the camera.</param>
		/// <param name="up">The vector the camera considers to be up.</param>
		/// <param name="forward">The vector the camera considers to be forward.</param>
		/// <param name="fieldOfView">The field of view of the camera.</param>
		/// <param name="aspectRatio">The aspect ratio of the screen.</param>
		/// <param name="nearPlane">The near plane of the camera.</param>
		/// <param name="farPlane">The far plane of the camera.</param>
		public Camera(Vector3 position, Vector3 up, Vector3 forward, 
			float fieldOfView, float aspectRatio, float nearPlane, float farPlane)
		{
			this.mPosition = position;
			this.mUp = up;
			this.mForward = Vector3.Normalize(forward);
			this.mTarget = mPosition + mForward;

			this.mFieldOfView = fieldOfView;
			this.mAspectRatio = aspectRatio;
			this.mNearPlane = nearPlane;
			this.mFarPlane = farPlane;

			this.mViewMatrix = Matrix.CreateLookAt(mPosition, mTarget, mUp);
			this.mProjectionMatrix = Matrix.CreatePerspectiveFieldOfView(mFieldOfView, mAspectRatio, mNearPlane, mFarPlane);

			if (mActive == null)
				mActive = this;
		}

		#endregion

		#region Properties

		/// <summary>
		/// Gets the view matrix of the camera. 
		/// </summary>
		public Matrix ViewMatrix
		{
			get { return mViewMatrix; }
		}

		/// <summary>
		/// Gets the projection matrix of the camera.
		/// </summary>
		public Matrix ProjectionMatrix
		{
			get { return mProjectionMatrix; }
		}

		/// <summary>
		/// Gets or sets the field of view (in radians) of the camera.
		/// </summary>
		public float FieldOfView
		{
			get { return mFieldOfView; }
			set 
			{
				mFieldOfView = MathHelper.Clamp(value, MIN_FOV, MAX_FOV);
				mProjectionMatrix = Matrix.CreatePerspectiveFieldOfView(mFieldOfView, mAspectRatio, mNearPlane, mFarPlane);
			}
		}

		/// <summary>
		/// Gets or sets the aspect ratio of the camera.
		/// </summary>
		public float AspectRatio
		{
			get { return mAspectRatio; }
			set 
			{
				mAspectRatio = value; 
				mProjectionMatrix = Matrix.CreatePerspectiveFieldOfView(mFieldOfView, mAspectRatio, mNearPlane, mFarPlane);
			}
		}

		/// <summary>
		/// Gets or sets the near plane of the camera.
		/// </summary>
		public float NearPlane
		{
			get { return mNearPlane; }
			set
			{
				mNearPlane = value;
				mProjectionMatrix = Matrix.CreatePerspectiveFieldOfView(mFieldOfView, mAspectRatio, mNearPlane, mFarPlane);
			}
		}

		/// <summary>
		/// Gets or sets the far plane of the camera.
		/// </summary>
		public float FarPlane
		{
			get { return mFarPlane; }
			set 
			{
				mFarPlane = value;
				mProjectionMatrix = Matrix.CreatePerspectiveFieldOfView(mFieldOfView, mAspectRatio, mNearPlane, mFarPlane);
			}
		}

		/// <summary>
		/// Gets the current rotation of the camera.
		/// </summary>
		public Quaternion Rotation
		{
			get { return Quaternion.CreateFromYawPitchRoll(mYaw, mPitch, mRoll); }
		}

		/// <summary>
		/// Gets the current camera position.
		/// </summary>
		public Vector3 Position
		{
			get { return mPosition; }
		}

		/// <summary>
		/// Gets or sets the currently active camera.
		/// </summary>
		public static Camera Active
		{
			get { return mActive; }
			set { mActive = value; }
		}

		#endregion

		#region Methods

		/// <summary>
		/// Moves the camera in the given movementDirection.
		/// </summary>
		/// <param name="movementDirection">The vector to move with</param>
		public void Move(Vector3 direction)
		{
			direction = Vector3.Transform(direction, Rotation);
			mPosition += direction;

			UpdateViewMatrix();
		}

		/// <summary>
		/// Pans (rotates around Y) the camera with the given angle.
		/// </summary>
		/// <param name="angleInDegrees">The angle to pan with</param>
		/// <remarks>Positive angles turn the camera to the right</remarks>
		public void Pan(float angleInDegrees)
		{
			mYaw -= MathHelper.ToRadians(angleInDegrees);
			if (mYaw > MathHelper.TwoPi)
				mYaw -= MathHelper.TwoPi;
			else if (mYaw < -MathHelper.TwoPi)
				mYaw += MathHelper.TwoPi;
			
			UpdateViewMatrix();
		}

		/// <summary>
		/// Tilts (rotates around X) the camera with the given angle. Positive angles turn the camera up
		/// </summary>
		/// <param name="angleInDegrees">The angle to tilt with</param>
		public void Tilt(float angleInDegrees)
		{
			mPitch += MathHelper.ToRadians(angleInDegrees);
			mPitch = MathHelper.Clamp(mPitch, -MathHelper.PiOver2 + 0.01f, MathHelper.PiOver2 - 0.01f);

			UpdateViewMatrix();
		}

		/// <summary>
		/// Rotates (causes the camera to orbit) the camera around the target. 
		/// </summary>
		/// <param name="rotation">A quaternion representing the rotation around the target.</param>
		public void RotateAroundTarget(Quaternion rotation)
		{
			mPosition = Vector3.Transform(mPosition, rotation);
			mViewMatrix = Matrix.CreateLookAt(mPosition, mTarget, mUp);
		}

		/// <summary>
		/// Zooms the camera in with the given angle. Positive values zoom in, negative zoom out.
		/// </summary>
		/// <param name="angleInDegrees">The angle to zoom with</param>
		public void Zoom(float angleInDegrees)
		{
			float angleInRads = MathHelper.ToRadians(angleInDegrees);
			FieldOfView -= angleInRads;
		}

		public override string ToString()
		{
			string value = String.Format("Position: {0}, Target: {1}, Up: {2}\nFoV: {3}, AspectRatio: {4}, Nearplane: {5}, FarPlane: {6}",
				this.mPosition, this.mTarget, this.mUp, this.mFieldOfView, this.mAspectRatio, this.mNearPlane, this.mFarPlane);

			return value;
		}

		/// <summary>
		/// Updates the target vector and the view matrix
		/// </summary>
		private void UpdateViewMatrix()
		{
			Vector3 transformedForwardVector = Vector3.Transform(mForward, Rotation);
			mTarget = mPosition + transformedForwardVector;
			mViewMatrix = Matrix.CreateLookAt(mPosition, mTarget, mUp);
		}

		#endregion
	}
}
